Skip to content

optable-targeting: Enhance Optable Targeting: Non-blocking network calls and granular traffic controls - #2

Open
softcoder594 wants to merge 29 commits into
masterfrom
optable-targeting-non-blocking-early-network-call
Open

optable-targeting: Enhance Optable Targeting: Non-blocking network calls and granular traffic controls#2
softcoder594 wants to merge 29 commits into
masterfrom
optable-targeting-non-blocking-early-network-call

Conversation

@softcoder594

@softcoder594 softcoder594 commented May 5, 2026

Copy link
Copy Markdown
Collaborator

Type of changes

  • module update

What's the context?

This update reduces auction latency by initiating the Optable API call at the earliest hook stage (raw-auction-request) and deferring the wait to the per-bidder stage (bidder-request), where the enrichment data is actually injected. It also adds granular controls over which traffic and which bidders receive enrichment.

New Features

  1. Non-Blocking Early Network Call: The API call starts in raw-auction-request and runs in parallel with other auction processing. The result is consumed per-bidder in bidder-request, avoiding pipeline blocking.
  2. Per-Bidder Enrichment Percentage: Configurable sampling rate per bidder (0-100%) to control what fraction of bid requests receive enrichment.
  3. Web vs. App Traffic Toggle: enrich-web / enrich-app flags to select which traffic sources are enriched.

Backwards Compatibility

The processed-auction-request hook is retained as a legacy fallback. If operators have not migrated to the new execution plan, the module behaves as before — enrichment happens synchronously in the processed hook.

New Configuration (recommended)

hooks:
  modules:
    optable-targeting:
      enrichment-percentage: 100
      bidder-enrichment-percentages:
        appnexus: 75
        rubicon: 75
        pubmatic: 100
        criteo: 0
      enrich-web: true
      enrich-app: true

  execution-plan:
    endpoints:
      "/openrtb2/auction":
        stages:
          raw-auction-request:
            groups:
              - timeout: 50
                hook-sequence:
                  - module-code: "optable-targeting"
                    hook-impl-code: "optable-targeting-raw-auction-request-hook"
          bidder-request:
            groups:
              - timeout: 50
                hook-sequence:
                  - module-code: "optable-targeting"
                    hook-impl-code: "optable-targeting-bidder-request-hook"
          auction-response:
            groups:
              - timeout: 10
                hook-sequence:
                  - module-code: "optable-targeting"
                    hook-impl-code: "optable-targeting-auction-response-hook"

Legacy Configuration (to be migrated)

The following execution plan fragment should be removed once migrated to the new configuration above:

          processed-auction-request:
            groups:
              - timeout: 600
                hook-sequence:
                  - module-code: "optable-targeting"
                    hook-impl-code: "optable-targeting-processed-auction-request-hook"

The processed-auction-request hook detects whether the new hooks (raw-auction-request and bidder-request) are present in the execution plan. If both are active, it passes through immediately without blocking the pipeline. If the new hooks are absent, it falls back to the legacy synchronous behavior. This means the legacy fragment can be kept during migration without negating the latency benefit of the new configuration.

Test plan

  • Unit Tests
  • Manual Integration Tests

Quality check

  • Are your changes following our code style guidelines?
  • Are there any breaking changes in your code? No
  • Does your test coverage exceed 90%?
  • Are there any erroneous console logs, debuggers or leftover code in your changes? No

@softcoder594 softcoder594 changed the title optable-targeting: implement Non-Blocking Early Network Call optable-targeting: Enhance Optable Targeting: Non-blocking network calls and granular traffic controls May 25, 2026
Comment thread sample/configs/prebid-config-with-optable-legacy.yaml Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest those validation functions are placed in the same package as OptableTargetingProperties. It will encourage reusability and reduce imports.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather keep the Validator class outside the models package.

@ericst-amand-optable ericst-amand-optable left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall. Submitted some small comments/refactor. I did not try to run the server, nor the tests, so this is pure code review for what it is.

@justadreamer
justadreamer marked this pull request as ready for review July 1, 2026 16:15
@softcoder594
softcoder594 force-pushed the optable-targeting-non-blocking-early-network-call branch from 134fd4e to 8e8ef64 Compare July 21, 2026 18:53

@eugenedorfman-optable eugenedorfman-optable left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the raw + bidder execution plan at enrichment-percentage: 100, the early Targeting API call is fired on every auction and nothing reads the result: OptableTargetingProcessedAuctionRequestHook returns early whenever both hooks are in the plan, and OptableBidderRequestHook drops every bidder before it touches the future. The cost is a real HTTP request plus a cache write per auction, no bid request enriched, nothing in the analytics tags, and a failed future with no handler attached, so failures are invisible.

This only bites if you deploy that plan at 100% with no bidder-enrichment-percentages. Cause and fix inline.

@eugenedorfman-optable eugenedorfman-optable left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isPerBidderEnrichmentEnabled() makes enrichment-percentage: 100 behave as though per-bidder enrichment were switched off, which is what leaves the early Targeting API call without a consumer on the raw + bidder plan. 100% should be just another sampling rate, no different from 50%. Suggestion inline.

Comment on lines +35 to +39
final OptableTargetingProperties properties = moduleContext.getOptableTargetingProperties();
if (!properties.isPerBidderEnrichmentEnabled()) {
return noAction(moduleContext, null);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guard is the special case that orphans the early Targeting API call, as described in the comment above.

biddersToEnrich already describes exactly what to enrich at any percentage: BidderEnrichmentSampler.sample excludes a bidder when its percentage is 0 and includes it when the percentage is 100, resolving per-bidder overrides before falling back to the default. The check on the next lines is therefore sufficient and uniform. This extra gate adds a second, inconsistent notion of "is per-bidder enrichment on", and it reads backwards: enrichment-percentage: 100 is the setting that asks for the most enrichment, and it is the one value that makes isPerBidderEnrichmentEnabled() false.

The effect on the raw + bidder plan is that at 100% every bidder is sampled in, the raw hook fires the call, and then every bidder is dropped here without the future ever being read.

Deleting the gate makes the behaviour uniform across percentages and gives the call its consumer back:

Suggested change
final OptableTargetingProperties properties = moduleContext.getOptableTargetingProperties();
if (!properties.isPerBidderEnrichmentEnabled()) {
return noAction(moduleContext, null);
}
final OptableTargetingProperties properties = moduleContext.getOptableTargetingProperties();

isPerBidderEnrichmentEnabled() in OptableTargetingProperties has no other caller, so it can go too.

Two things to sanity check before taking it. First, at 100% on the raw + bidder plan this turns bid request enrichment on where it is currently off, which is presumably the intent but is a live behaviour change. Second, if the gate was there to keep some other execution plan on the old whole-request path, that is worth stating, though OptableTargetingProcessedAuctionRequestHook only defers to the bidder hook when both hooks are in the plan, so I could not find such a path.

Worth a test either way: raw + bidder plan at enrichment-percentage: 100 should assert that every sampled bidder is enriched.

…ntage property is specified and it's value is 100%

@eugenedorfman-optable eugenedorfman-optable left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guard removal is right: isPerBidderEnrichmentEnabled() and its caller are both gone, nothing else referenced it, and the early Targeting API call now has a consumer at enrichment-percentage: 100. 147 tests pass.

The @JsonProperty("enrichment-percentage") in the same commit is a separate and real fix: per-account overrides of that key were being silently dropped. Two sibling fields still have it, inline below.

Comment on lines +52 to 55
Boolean enrichWeb = true;

Boolean enrichApp = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enrichWeb and enrichApp have the same binding bug that @JsonProperty("enrichment-percentage") just fixed on the field above.

ConfigResolver.resolve merges the per-account node over mapper.valueToTree(globalProperties) and deserializes the result with the prebid mapper, which sets PropertyNamingStrategies.SNAKE_CASE. Unannotated multi-word fields therefore bind to enrich_web and enrich_app, while README.md documents enrich-web and enrich-app as the account config keys (lines 105-106 for JSON, 128-129 for YAML). An account key spelled the documented way is an unknown property, gets dropped, and the account silently keeps the global value.

Global YAML is unaffected either way, since Spring relaxed binding handles kebab-case. This only bites per-account overrides, which is exactly why it goes unnoticed.

Suggested change
Boolean enrichWeb = true;
Boolean enrichApp = true;
}
@JsonProperty("enrich-web")
Boolean enrichWeb = true;
@JsonProperty("enrich-app")
Boolean enrichApp = true;
}

Verified: module suite stays green with this applied. Worth a ConfigResolver test covering per-account overrides of all four documented keys, since none of them has one today.

The account config node is deserialized with the prebid mapper, which uses
SNAKE_CASE, so the unannotated fields bound to enrich_web and enrich_app while
the documented account keys are enrich-web and enrich-app. Per-account
overrides of both were silently dropped and the global value kept.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants